package com.dsr.practice.wrapper;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import org.mozilla.javascript.Context;
import org.mozilla.javascript.NativeArray;
import org.mozilla.javascript.NativeObject;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;

import com.dsr.practice.wrapper.javascript.ProblemInfo;
import com.dsr.practice.wrapper.state.ApplicationMode;

public class Main {
	private static final int EOF = -1;
	private static final String JSLINT_PATH = "/jslint.js";
	private String jslintSource;
	private Context jsContext;

	/**
	 * @param args
	 * @throws Throwable 
	 */
	public static void main(String[] args) throws Throwable {
		ApplicationMode mode = parseCommandArguments(args);
		Main application = new Main();
		int resultCode = application.run(mode);
		System.exit(resultCode);
	}

	private int run(ApplicationMode mode) throws Throwable {
		if (mode.isShowHelp()) {
			showHelp();
			return 0;
		}
		if (mode.isShowVersion()) {
			showVersion();
			return 0;
		}
	
		int resultCode = 0;
		try {
			initializeApplication();
			ContentFileReader reader = new ContentFileReader();

			for (String fileName : mode.getFileNames()) {
				if (processFile(reader, fileName) == -1) {
					resultCode = -1;
				}
			}
		} catch (Throwable e) {
			resultCode = -1;
			throw e;
		} finally {
			postRunApplication();
		}
		
		return resultCode;
	}

	private void postRunApplication() {
		if (jsContext != null) {
			Context.exit();
		}
	}

	private void initializeApplication() throws IOException {
		jslintSource = readJsLintSource();
		jsContext = initializeJslintContext();
	}

	private Context initializeJslintContext() throws IOException {
		Context cx = Context.enter();
		// TODO probable performance issue

		Scriptable scope = cx.initStandardObjects();
		cx.evaluateString(scope, jslintSource, "<cmd>", 1, null);
		return cx;
	}

	private void showVersion() {
		// Shows the version of the program
		System.out.println("VERSION 1.0");
	}

	private void showHelp() {
		// Shows the manual of the program
		
		
		
		
		System.out.println("Usage: Main [-options] [args...]");
		System.out.println("where options include:");
		System.out.println("-h                 print this help message");
		System.out.println("-v                 shows version of the program");
		System.out.println("-f [args...]       check JS files for errors and shows result:");	
		System.out.println("                      0,if files have not an error");
		System.out.println("                      -1,if at least one file has an error");
	}

	private int processFile(ContentFileReader reader, String fileName) {
		try {
			String s = reader.read(fileName);
			List<ProblemInfo> problems = checkSource(s,fileName);
			if (problems.isEmpty()) {
				return 0;
			} else {
				ProblemInfo info;
				for (int i = 0; i < problems.size(); i++) {
					info = problems.get(i);
					
					System.out.println("Filename: "+info.getFileName());
					System.out.println("Number of line: "+info.getLine());
					System.out.println("Cause of error: "+info.getReason());
					System.out.println("Evidence: "+info.getEvidence());
					System.out.println("--------------------------------");
				}
				return -1;
			}

		} catch (IOException e) {
			System.err.println("File " + fileName
					+ " could not be read. Skipped.");
			return -1;
		}
	}

	private static ApplicationMode parseCommandArguments(String[] args) {
		ApplicationMode mode = new ApplicationMode();
		boolean filesBegan = false;

		for (String arg : args) {
			if (filesBegan) {
				mode.getFileNames().add(arg);
			} else {
				if (arg.equals("-f")) {
					filesBegan = true;
				} else if (arg.equals("-h")) {
					mode.setShowHelp(true);
				} else if (arg.equals("-v")) {
					mode.setShowVersion(true);
				}
			}
		}
		return mode;
	}

	private String readJsLintSource() throws IOException {
		StringBuilder sb = new StringBuilder();
		BufferedInputStream buf = null;
		try {
			InputStream stream = Main.class.getResourceAsStream(JSLINT_PATH);
			buf = new BufferedInputStream(stream);
			int c;
			while ((c = buf.read()) != EOF) {
				sb.append((char) c);
			}
		} finally {
			if (buf != null) {
				buf.close();
			}
		}
		return sb.toString();
	}
	
	private List<ProblemInfo> checkSource(String sourceString, String fileName)
			throws IOException {
		List<ProblemInfo> resultList = new ArrayList<ProblemInfo>();

		Context cx = Context.enter();
		try {
			Scriptable scope = cx.initStandardObjects();
			cx.evaluateString(scope, jslintSource, "<cmd>", 1, null);
			Object wrappedSourceStr = Context.javaToJS(sourceString, scope);
			ScriptableObject.putProperty(scope, "source", wrappedSourceStr);

			cx.evaluateString(scope, "var x = JSLINT(source);", "<cmd>", 2,
					null);
			Object value = scope.get("x", scope);
			if (value instanceof Boolean) {
				Boolean result = (Boolean) value;
				if (!result) {
					cx.evaluateString(scope,
							"var myData = JSLINT.data().errors;", "<cmd>", 1,
							null);
					Object errors = scope.get("myData", scope);

					if (errors instanceof NativeArray) {
						NativeArray errorArray = (NativeArray) errors;
						long len = errorArray.getLength();

						for (int i = 0; i < len; ++i) {
							Object obj = errorArray.get(i);
							ProblemInfo info = parseProblemInfo(obj,fileName);
							resultList.add(info);
						}
					} else {
						throw new RuntimeException(
								"Incompatible JSLint version");
					}
				}
			}

		} finally {
			Context.exit();
		}
		return resultList;
	}

	private ProblemInfo parseProblemInfo(Object obj, String fileName) {
		ProblemInfo info = new ProblemInfo();

		if (obj instanceof NativeObject) {
			NativeObject nativeObject = (NativeObject) obj;
   
			Double d = (Double) nativeObject.get("line"); 
			int i = (int) d.doubleValue();
			info.setLine(i);
			
			info.setFileName(fileName);
			info.setEvidence((String) nativeObject.get("evidence"));
			info.setReason((String) nativeObject.get("reason"));
		}
		return info;

	}

}
